package com.swotxu.mall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.swotxu.common.utils.PageUtils;
import com.swotxu.common.utils.Query;
import com.swotxu.mall.product.dao.CategoryDao;
import com.swotxu.mall.product.entity.CategoryEntity;
import com.swotxu.mall.product.service.CategoryBrandRelationService;
import com.swotxu.mall.product.service.CategoryService;
import com.swotxu.mall.product.web.vo.Catelog2Vo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    // 原执性检查并释放锁脚本
    public static final String REDIS_UNLOCK_SCRIPT = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    // 三级缓存数据 key
    private static final String REDIS_CATELOG_JSON_KEY = "catelogJson";
    // 三级缓存数据过期时间 min
    private static final int REDIS_CATELOG_JSON_KEY_TIMEOUT = 2 * 60;
    // 分布式锁 key
    private static final String REDIS_LOCK_CATELOG_JSON_KEY = "_catelogJsonLock";
    // 分布式锁过期时间 s
    private static final int REDIS_LOCK_CATELOG_JSON_KEY_TIMEOUT = 1 * 60;

    // 无需注入对应 Dao接口，默认注入了 baseMapper
    //@Autowired
    //private CategoryDao categoryDao;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    RedissonClient redissonClient;

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        // 1、查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        // 2、组装成父子的树形结构
        List<CategoryEntity> level1Menus = entities.stream()
                .filter(entity -> entity.getParentCid() == 0)
                .map(level1Menu -> {
                    List<CategoryEntity> childrens = getChildrens(level1Menu, entities);
                    level1Menu.setChildrens(childrens);
                    return level1Menu;
                })
                .sorted(Comparator.comparingInt(CategoryEntity::getSort))
                .collect(Collectors.toList());
        return level1Menus;
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {
        //TODO 1、检查当前删除的菜单，是否被其他地方引用
        baseMapper.deleteBatchIds(asList);
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList();
        // 迭代出所有父节点
        /*Long tempCatelogId = catelogId;
        while (tempCatelogId != 0) {
            paths.add(tempCatelogId);
            CategoryEntity entity = this.getById(tempCatelogId);
            tempCatelogId = entity.getParentCid();
        }*/
        // 简写迭代
        for (Long temp = catelogId; catelogId != 0 && paths.add(temp) && (temp = this.getById(temp).getParentCid()) != 0; );
        // 逆序数组
        Collections.reverse(paths);

        return paths.toArray(new Long[paths.size()]);
    }

    /**
     * 使用 @CacheEvict 进行缓存失效模式使用
     * 多个缓存失效操作的两种方式
     * 1. 使用 @Caching 进行多个缓存失效操作
     * 2. 使用 @CacheEvict(value = {"category"}, allEntries = true) 删除某个分区下的所有缓存
     *
     * 约定：存储同一类型的数据，指定为同一分区
     * @param category
     */
    /*@Caching(evict = {
            @CacheEvict(value = {"category"}, key = "'getLevel1Categorys'"),
            @CacheEvict(value = {"category"}, key = "'getCatelogJson'")
    })*/
    @CacheEvict(value = {"category"}, allEntries = true)    // 失效模式
    //@CachePut   // 双写模式使用此注解
    @Override
    @Transactional
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        // 保证关联表冗余字段数据一致
        if (StringUtils.isNotEmpty(category.getName())){
            // 更新 pms_category_brand_relation 表
            categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());

            //TODO 更新其他关联表
        }
        // 缓存数据修改或删除
        // stringRedisTemplate.delete(REDIS_CATELOG_JSON_KEY);
    }

    /**
     * 1、每个需要缓存的数据都来指定放到哪个名字的缓存【缓存分区（按照业务类型分）】
     * 2、@Cacheable({"category"})
     *      代表当前方法的结果需要缓存，如果缓存中有，则直接返回缓存数据，不调用方法。如果没有，则调用方法并放入缓存
     * 3、默认行为
     *      1）如果缓存有，则方法不调用
     *      2）默认生成的缓存key -> category::SimpleKey []
     *      3）默认生成的缓存value -> 使用 JDK序列化机制
     *      4）默认过期时间（ttl）-1
     * 4、自定义
     *      1）指定key：使用 key属性指定，接收一个 SpEL表达式(#root.method.name)。若写字符串需 "'level1Categorys'"
     *      2）指定过期时间：配置文件修改ttl -> spring.cache.time-to-live: 3600000 (ms)
     *      3）指定序列化
     *
     * @return
     */
    @Cacheable(value = {"category"}, key = "#root.methodName")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        log.info("获取一级分类数据..");
        return this.baseMapper.selectList(new LambdaQueryWrapper<CategoryEntity>().eq(CategoryEntity::getParentCid, 0));
    }

    @Cacheable(value = {"category"}, key = "#root.methodName", sync = true)
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        log.info("查询三级分类数据..");
        return getCatelogJsonFromDB();
    }
    /**
     * TODO 产生堆外内存溢出异常
     * Redis exception; nested exception is io.lettuce.core.RedisException: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 58720256 byte(s) of direct memory (used: 71303168, max: 125829120)
     * 原因：
     * 1）SpringBoot2.0 以后，默认使用 lettuce5.x 作为操作Redis的客户端，其内部使用的 netty进行网络通信
     * 2）lettuce 的 bug导致堆外内存溢出（-Xmx128m netty默认使用最大堆大小作为堆外内存）
     *    可通过 -Dio.netty.maxDirectMemory 设置
     * 解决方案：不能只调大内存处理
     * 1）升级 lettuce 客户端
     * 2）切换使用 jedis作为 Redis客户端
     *
     * 本地锁 synchronized (this)
     * 分布式锁 Redis
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatelogJson2() {
//        return getCatelogJsonWithRedisLock();
        return getCatelogJsonWithRedissonLock();
    }

    /**
     * 利用 Redisson 分布式锁框架实现缓存逻辑
     * 缓存数据一致性模式
     * 1）失效模式
     * 2）双写模式
     * @return
     */
    private Map<String, List<Catelog2Vo>> getCatelogJsonWithRedissonLock() {
        // 锁的粒度：具体缓存的是某个数据
        String catelogJsonStr = stringRedisTemplate.opsForValue().get(REDIS_CATELOG_JSON_KEY);
        if (StringUtils.isEmpty(catelogJsonStr)) {
            RLock lock = redissonClient.getLock(REDIS_LOCK_CATELOG_JSON_KEY);
            lock.lock();
            try {
                Map<String, List<Catelog2Vo>> catelogJson = getCatelogJsonFromDB();
                stringRedisTemplate.opsForValue().set(REDIS_CATELOG_JSON_KEY, JSON.toJSONString(catelogJson)
                        , REDIS_CATELOG_JSON_KEY_TIMEOUT, TimeUnit.MINUTES);
                return catelogJson;
            } finally {
                lock.unlock();
            }
        }
        return JSON.parseObject(catelogJsonStr, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
    }

    /**
     * 手动实现双锁逻辑
     * @return
     */
    private Map<String, List<Catelog2Vo>> getCatelogJsonWithRedisLock() {
        String catelogJsonStr = stringRedisTemplate.opsForValue().get(REDIS_CATELOG_JSON_KEY);
        if (StringUtils.isEmpty(catelogJsonStr)) {
            // 1. 获取本地锁
            synchronized (this) {
                log.info("Get local lock success!{}", Thread.currentThread().getName());
                catelogJsonStr = stringRedisTemplate.opsForValue().get(REDIS_CATELOG_JSON_KEY);
                if (StringUtils.isEmpty(catelogJsonStr)) {
                    // 2. 获取分布式 Redis锁
                    String token = Thread.currentThread().getName() + System.currentTimeMillis();
                    // 2.1 加锁和设置过期时间需保证原子性
                    if (stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_CATELOG_JSON_KEY, token
                            , REDIS_LOCK_CATELOG_JSON_KEY_TIMEOUT, TimeUnit.SECONDS)) {
                        log.info("Get redis lock success!{}", Thread.currentThread().getName());
                        try {
                            log.info("三级菜单缓存未命中，查询数据库!");
                            Map<String, List<Catelog2Vo>> catelogJson = getCatelogJsonFromDB();
                            stringRedisTemplate.opsForValue().set(REDIS_CATELOG_JSON_KEY, JSON.toJSONString(catelogJson)
                                    , REDIS_CATELOG_JSON_KEY_TIMEOUT, TimeUnit.MINUTES);
                            return catelogJson;
                        } finally {
                            // 2.2 释放锁和校验是否为自己锁，需保证原子性
                            /*String val = stringRedisTemplate.opsForValue().get(REDIS_LOCK_CATELOG_JSON_KEY);
                            if (token.equals(val)) {
                                stringRedisTemplate.delete(REDIS_LOCK_CATELOG_JSON_KEY);
                            }*/
                            stringRedisTemplate.execute(new DefaultRedisScript<Long>(REDIS_UNLOCK_SCRIPT, Long.class)
                                    , Arrays.asList(REDIS_LOCK_CATELOG_JSON_KEY), token);
                        }
                    } else {
                        // 2.3 分布式锁获取失败，wait让出锁，sleep不释放锁，等待后重新获取（这里使用sleep，避免本地锁的多次竞争）
                        log.info("Get redis lock failed!{}", Thread.currentThread().getName());
                        try {
                            // this.wait(200);
                            TimeUnit.MILLISECONDS.sleep(200);
                        } catch (InterruptedException e) {
                            log.warn("等待异常！", e);
                        }
                        return getCatelogJson();
                    }
                }
            }
        }
        return JSON.parseObject(catelogJsonStr, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
    }

    /**
     * 从数据库查询并封装分类数据
     * @return
     */
    private Map<String, List<Catelog2Vo>> getCatelogJsonFromDB() {
        // 查出全部三级分类，并转换数据格式
        return this.listWithTree().stream().collect(Collectors.toMap(
                entity -> entity.getCatId().toString(),
                // Value解析为 List<Catelog2Vo>
                entity -> Optional.ofNullable(entity.getChildrens())
                        .map(category2List -> category2List.stream()
                            .map(category2 -> new Catelog2Vo(
                                category2.getParentCid().toString()
                                , category2.getCatId().toString()
                                , category2.getName()
                                // 解析 Catelog2Vo 中的 List<Catelog2Vo.Catelog3Vo> 属性
                                , Optional.ofNullable(category2.getChildrens())
                                    .map(category3List -> category3List.stream()
                                        .map(category3 -> new Catelog2Vo.Catelog3Vo(
                                                category3.getParentCid().toString()
                                                , category3.getCatId().toString()
                                                , category3.getName()))
                                        .collect(Collectors.toList()))
                                    .get()
                                )
                            ).collect(Collectors.toList()))
                        .get()
            ));
    }

    /**
     * 从全部集合中，找出指定元素的下级孩子
     * @param root  待查找下级的目标元素
     * @param all   所有元素列表
     * @return
     */
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all){
        return all.stream()
                .filter(entity -> entity.getParentCid().equals(root.getCatId()))
                .peek(entity -> {
                    if(entity.getSort() == null) {
                        entity.setSort(0);
                    }
                })
                .map(currMenu -> {
                    List<CategoryEntity> childrens = getChildrens(currMenu, all);
                    currMenu.setChildrens(childrens);
                    return currMenu;
                })
                .sorted(Comparator.comparingInt(CategoryEntity::getSort))
                .collect(Collectors.toList());
    }

}